import { IHeapSnapshot, IHeapNode, LeakTracePathItem } from '@memlab/core'
import { Finder } from '../utils/Finder';
import { ILoggable, Log } from '../utils/Log';

/**
 * ArkTracePathItem
 */
export type ArkTracePathItem = LeakTracePathItem;

/**
 * ArkNodeTracePath
 */
export class ArkNodeTracePath {
    public readonly tracePathItem: ArkTracePathItem;
    public readonly uid: string;
    public readonly nodeIds: number[];
    public readonly reverse: boolean;
    public readonly retainedSize: number;

    constructor(item: ArkTracePathItem, reverse: boolean = false) {
        this.tracePathItem = item;
        this.nodeIds = ArkNodeTracePath.getNodeIdTrace(item);
        this.retainedSize = ArkNodeTracePath.getRetainedSize(item);
        this.reverse = reverse;
        this.uid = reverse ? this.nodeIds.reverse().join('<-') : this.nodeIds.join('->');
    }

    public getTracePathInfo(joinStr: string = '===>') : string {
        if (this.nodeIds.length === 0) {
            return '';
        }
        const snapshot = this.tracePathItem.node?.snapshot;
        if (!snapshot) {
            Log.error('meminsight', ArkNodeTracePath.name, 'Failed to get snapshot.');
            return '';
        }
        const result: string[] = [];
        this.nodeIds.forEach(id => {
            let node = snapshot.getNodeById(id);
            if (node) {
                let sizeStr = node.retainedSize >= 1024 * 1024 ?
                    (node.retainedSize / (1024 * 1024)).toFixed(2) + 'MB' :
                    (node.retainedSize / 1024).toFixed(2) + 'KB';
                result.push(`@${id} (${node.type}) [${sizeStr}] ${node.name}`)
            } 
        })
        return result.join(`\n${joinStr}`);
    }

    public static getNodeIdTrace(item: ArkTracePathItem) : number[] {
        const nodeIds: number[] = [];
        let curItem : ArkTracePathItem | undefined = item;
        while (curItem) {
            let node: IHeapNode | undefined = curItem.node;
            if (!node) {
                break;
            }
            nodeIds.push(node.id);
            curItem = curItem.next;
        }
        return nodeIds;
    }

    /**
     * 获取本路径上的节点retained size
     * @param item 
     * @returns 
     */
    public static getRetainedSize(item: ArkTracePathItem) : number {
        let retained: number = 0;
        let curItem : ArkTracePathItem | undefined = item;
        while (curItem) {
            let node: IHeapNode | undefined = curItem.node;
            if (!node) {
                break;
            }
            retained += node.self_size;
            curItem = curItem.next;
        }
        return retained;
    }
}

/**
 * ArkNode最短路径uid和最短路径索引关系对象
 */
export class ArkNodePathIndexes {
    public readonly uid: string;
    public indexes: number[];

    constructor(uid: string) {
        this.uid = uid;
    }

    public addIndex(index: number) {
        this.indexes.push(index);
    }

    public get(filter: (condition: any) => boolean, condition: any) {
        return this.indexes.filter((index) => {
            return filter(condition);
        });
    }
}

/**
 * ArkNodes
 */
export class ArkNodes extends Array<number> implements ILoggable {
    public readonly DOMAIN: string = 'meminsight';
    public readonly TAG: string = ArkNodes.name;
    public className: string = '';         // node class name
    public selfSize: number = 0;           // node total self size
    public retainedSize: number = 0;       // node total retained size
    public shortestPaths: ArkNodeTracePath[]; // shortest path array of all node with same calss name
    public aggrationPathIndexes: Map<string, ArkNodePathIndexes>;  // <path uid, path indexes>

    /**
     * ArkNodes constructor
     * @param className node name
     * @param selfSize node self size
     * @param retainedSize node retained size
     */
    constructor(className: string, selfSize: number, retainedSize: number) {
        super();
        this.className = className;
        this.selfSize = selfSize;
        this.retainedSize = retainedSize;
    }

    /**
     * add shortest node trace path
     * @param path shortest node trace path
     */
    public addShortestPath(path: ArkNodeTracePath) : void {
        this.shortestPaths.push(path);
    }

    /**
     * add node id
     * @param nodeId node id
     */
    public addNodeId(nodeId: number) : void {
        this.push(nodeId);
    }

    /**
     * 获取最短路径uid和最短路径索引和引用次数的映射数据
     * uid = [...node.id].join('->')
     * @param descending 按引用次数降序排序，否则升序排序
     * @param maxIndexLength 最大引用路径个数
     */
    public getAggrationPathIndexes(descending: boolean = true, 
        maxIndexLength: number = -1, minRetainedSize = -1) : void {
        if (this.shortestPaths.length === 0) {
            Log.warnX(this, 'No shortest path found');
            return;
        }
        const indexes = new Map<string, ArkNodePathIndexes>();
        this.shortestPaths.forEach((path, index) => {
            const indexesItem = indexes.get(path.uid);
            if (!indexesItem) {
                const pathIndexes = new ArkNodePathIndexes(path.uid);
                pathIndexes.addIndex(index);
                indexes.set(path.uid, pathIndexes);
            } else {
                indexesItem.addIndex(index);
            }
        });
        const sortedIndexes = descending 
            ? new Map([...indexes.entries()].sort((a, b) => a[1].indexes.length - b[1].indexes.length))
            : new Map([...indexes.entries()].sort((a, b) => b[1].indexes.length - a[1].indexes.length));
        if ((maxIndexLength <= 0) || (sortedIndexes.size <= maxIndexLength)) {
            this.aggrationPathIndexes = sortedIndexes;
        } else {
            this.aggrationPathIndexes = new Map([...sortedIndexes.entries()].slice(0, maxIndexLength));
        }
        if (minRetainedSize > 0) {
            this.aggrationPathIndexes.forEach((pathIndexes, uid) => {
                const tmpIndexes: number[] = [];
                let idx = pathIndexes.indexes.pop();
                while (idx) {
                    let size = this.shortestPaths.at(idx)?.retainedSize || 0;
                    if (size >= minRetainedSize) {
                        tmpIndexes.push(idx);
                    }
                    idx = pathIndexes.indexes.pop();
                }
                pathIndexes.indexes = tmpIndexes;
            });
        }
    }

    public getPathIndexesRetainedSize(uid: string) : number {
        let pathIndexes = this.aggrationPathIndexes.get(uid);
        if (!pathIndexes) {
            return 0;
        }
        let preSize = 0;
        let curSize: number | undefined;
        return pathIndexes.indexes.reduce((pre, cur) => {
            curSize = this.shortestPaths.at(cur)?.retainedSize;
            if (!curSize) {
                return preSize;
            }
            preSize = curSize;
            return preSize + curSize;
        }, 0);
    }
}

/**
 * ArkNodes Map<nodeName, nodeId array>
 */
export class ArkNodesMap extends Map<string, ArkNodes> implements ILoggable {
    DOMAIN: string = 'meminsight'
    TAG: string = ArkNodesMap.name;

    constructor() {
        super();
    }

    /**
     * add node to map
     * @param snapshot heap snapshot
     * @param node heap node
     * @param nameWhiteList node names
     * @param typeWhiteList node types
     * @param enableShortestPath enable shortest path
     */
    public addNode(
        snapshot: IHeapSnapshot,
        node: IHeapNode,
        nameWhiteList: string[],
        typeWhiteList: string[] = ['object'],
        enableShortestPath: boolean = true) : void {
        // true if names is empty or types include node.type
        if ((nameWhiteList.length == 0 
            || nameWhiteList.includes(node.name)) 
            && typeWhiteList.includes(node.type)) {
            const arkNodes: ArkNodes | undefined = this.get(node.name);
            if (!arkNodes) {
                let arkNodes: ArkNodes = new ArkNodes(node.name, node.self_size, node.retainedSize);
                if (enableShortestPath) {
                    const arkTracePathItem: ArkTracePathItem | undefined = Finder.findShortestPathToGCRoot(snapshot, node);
                    if (arkTracePathItem) {
                        arkNodes.addShortestPath(new ArkNodeTracePath(arkTracePathItem));
                    } else {
                        Log.errorX(this, 'ArkNodesMap1: Finder.findShortestPathToGCRoot return undefined.');
                    }
                }
                arkNodes.addNodeId(node.id);
                this.set(node.name, arkNodes);
            } else { 
                arkNodes.selfSize += node.self_size;
                arkNodes.retainedSize += node.retainedSize;
                if (enableShortestPath) {
                    const arkTracePathItem: ArkTracePathItem | undefined = Finder.findShortestPathToGCRoot(snapshot, node);
                    if (arkTracePathItem) {
                        arkNodes.addShortestPath(new ArkNodeTracePath(arkTracePathItem));
                    } else {
                        Log.errorX(this, 'ArkNodesMap2: Finder.findShortestPathToGCRoot return undefined.');
                    }
                }
                arkNodes.addNodeId(node.id);
            }
        }
    }
}